// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/web_contents/aura/gesture_nav_simple.h"

#include <utility>

#include "base/macros.h"
#include "cc/layers/layer.h"
#include "content/browser/frame_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/overscroll_controller.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/web_contents/web_contents_view.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/overscroll_configuration.h"
#include "content/public/common/content_client.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image.h"
#include "ui/resources/grit/ui_resources.h"

namespace content {

namespace {

    const int kArrowHeight = 280;
    const int kArrowWidth = 140;
    const float kMinOpacity = 0.25f;

    bool ShouldNavigateForward(const NavigationController& controller,
        OverscrollMode mode)
    {
        return mode == (base::i18n::IsRTL() ? OVERSCROLL_EAST : OVERSCROLL_WEST) && controller.CanGoForward();
    }

    bool ShouldNavigateBack(const NavigationController& controller,
        OverscrollMode mode)
    {
        return mode == (base::i18n::IsRTL() ? OVERSCROLL_WEST : OVERSCROLL_EAST) && controller.CanGoBack();
    }

    // An animation observers that deletes itself and a pointer after the end of the
    // animation.
    template <class T>
    class DeleteAfterAnimation : public ui::ImplicitAnimationObserver {
    public:
        explicit DeleteAfterAnimation(std::unique_ptr<T> object)
            : object_(std::move(object))
        {
        }

    private:
        friend class base::DeleteHelper<DeleteAfterAnimation<T>>;

        ~DeleteAfterAnimation() override { }

        // ui::ImplicitAnimationObserver:
        void OnImplicitAnimationsCompleted() override
        {
            // Deleting an observer when a ScopedLayerAnimationSettings is iterating
            // over them can cause a crash (which can happen during tests). So instead,
            // schedule this observer to be deleted soon.
            BrowserThread::DeleteSoon(BrowserThread::UI, FROM_HERE, this);
        }

        std::unique_ptr<T> object_;
        DISALLOW_COPY_AND_ASSIGN(DeleteAfterAnimation);
    };

} // namespace

// A layer delegate that paints the shield with the arrow in it.
class ArrowLayerDelegate : public ui::LayerDelegate {
public:
    explicit ArrowLayerDelegate(int resource_id)
        : image_(GetContentClient()->GetNativeImageNamed(resource_id))
        , left_arrow_(resource_id == IDR_BACK_ARROW)
    {
        CHECK(!image_.IsEmpty());
    }

    ~ArrowLayerDelegate() override { }

    bool left() const { return left_arrow_; }

private:
    // ui::LayerDelegate:
    void OnPaintLayer(const ui::PaintContext& context) override
    {
        SkPaint paint;
        paint.setColor(SkColorSetARGB(0xa0, 0, 0, 0));
        paint.setStyle(SkPaint::kFill_Style);
        paint.setAntiAlias(true);

        // Set the recording size to be the size of the |arrow_| layer, and draw a
        // half circle (the other half will be clipped), then an arrow image inside
        // it.
        ui::PaintRecorder recorder(context, gfx::Size(kArrowWidth, kArrowHeight));
        recorder.canvas()->DrawCircle(
            gfx::Point(left_arrow_ ? 0 : kArrowWidth, kArrowHeight / 2),
            kArrowWidth, paint);
        recorder.canvas()->DrawImageInt(
            *image_.ToImageSkia(), left_arrow_ ? 0 : kArrowWidth - image_.Width(),
            (kArrowHeight - image_.Height()) / 2);
    }

    void OnDelegatedFrameDamage(const gfx::Rect& damage_rect_in_dip) override { }

    void OnDeviceScaleFactorChanged(float device_scale_factor) override { }

    const gfx::Image& image_;
    const bool left_arrow_;

    DISALLOW_COPY_AND_ASSIGN(ArrowLayerDelegate);
};

GestureNavSimple::GestureNavSimple(WebContentsImpl* web_contents)
    : web_contents_(web_contents)
    , completion_threshold_(0.f)
{
}

GestureNavSimple::~GestureNavSimple() { }

void GestureNavSimple::ApplyEffectsAndDestroy(const gfx::Transform& transform,
    float opacity)
{
    ui::Layer* layer = arrow_.get();
    ui::ScopedLayerAnimationSettings settings(arrow_->GetAnimator());
    settings.AddObserver(
        new DeleteAfterAnimation<ArrowLayerDelegate>(std::move(arrow_delegate_)));
    settings.AddObserver(new DeleteAfterAnimation<ui::Layer>(std::move(arrow_)));
    settings.AddObserver(
        new DeleteAfterAnimation<ui::Layer>(std::move(clip_layer_)));
    layer->SetTransform(transform);
    layer->SetOpacity(opacity);
}

void GestureNavSimple::AbortGestureAnimation()
{
    if (!arrow_)
        return;
    gfx::Transform transform;
    transform.Translate(arrow_delegate_->left() ? -kArrowWidth : kArrowWidth, 0);
    ApplyEffectsAndDestroy(transform, kMinOpacity);
}

void GestureNavSimple::CompleteGestureAnimation()
{
    if (!arrow_)
        return;
    // Make sure the fade-out starts from the complete state.
    ApplyEffectsForDelta(completion_threshold_);
    ApplyEffectsAndDestroy(arrow_->transform(), 0.f);
}

bool GestureNavSimple::ApplyEffectsForDelta(float delta_x)
{
    if (!arrow_)
        return false;
    CHECK_GT(completion_threshold_, 0.f);
    CHECK_GE(delta_x, 0.f);
    double complete = std::min(1.f, delta_x / completion_threshold_);
    float translate_x = gfx::Tween::FloatValueBetween(complete, -kArrowWidth, 0);
    gfx::Transform transform;
    transform.Translate(arrow_delegate_->left() ? translate_x : -translate_x,
        0.f);
    arrow_->SetTransform(transform);
    arrow_->SetOpacity(gfx::Tween::FloatValueBetween(complete, kMinOpacity, 1.f));
    return true;
}

gfx::Rect GestureNavSimple::GetVisibleBounds() const
{
    return web_contents_->GetNativeView()->bounds();
}

bool GestureNavSimple::OnOverscrollUpdate(float delta_x, float delta_y)
{
    return ApplyEffectsForDelta(std::abs(delta_x) + 50.f);
}

void GestureNavSimple::OnOverscrollComplete(OverscrollMode overscroll_mode)
{
    CompleteGestureAnimation();

    NavigationControllerImpl& controller = web_contents_->GetController();
    if (ShouldNavigateForward(controller, overscroll_mode))
        controller.GoForward();
    else if (ShouldNavigateBack(controller, overscroll_mode))
        controller.GoBack();
}

void GestureNavSimple::OnOverscrollModeChange(OverscrollMode old_mode,
    OverscrollMode new_mode)
{
    NavigationControllerImpl& controller = web_contents_->GetController();
    if (!ShouldNavigateForward(controller, new_mode) && !ShouldNavigateBack(controller, new_mode)) {
        AbortGestureAnimation();
        return;
    }

    arrow_.reset(new ui::Layer(ui::LAYER_TEXTURED));
    // Note that RTL doesn't affect the arrow that should be displayed.
    int resource_id = 0;
    if (new_mode == OVERSCROLL_WEST)
        resource_id = IDR_FORWARD_ARROW;
    else if (new_mode == OVERSCROLL_EAST)
        resource_id = IDR_BACK_ARROW;
    else
        NOTREACHED();

    arrow_delegate_.reset(new ArrowLayerDelegate(resource_id));
    arrow_->set_delegate(arrow_delegate_.get());
    arrow_->SetFillsBoundsOpaquely(false);

    aura::Window* window = web_contents_->GetNativeView();
    const gfx::Rect& window_bounds = window->bounds();
    completion_threshold_ = window_bounds.width() * GetOverscrollConfig(OVERSCROLL_CONFIG_HORIZ_THRESHOLD_COMPLETE);

    // Align on the left or right edge.
    int x = (resource_id == IDR_BACK_ARROW) ? 0 : (window_bounds.width() - kArrowWidth);
    // Align in the center vertically.
    int y = std::max(0, (window_bounds.height() - kArrowHeight) / 2);
    arrow_->SetBounds(gfx::Rect(x, y, kArrowWidth, kArrowHeight));
    ApplyEffectsForDelta(0.f);

    // Adding the arrow as a child of the content window is not sufficient,
    // because it is possible for a new layer to be parented on top of the arrow
    // layer (e.g. when the navigated-to page is displayed while the completion
    // animation is in progress). So instead, a clip layer (that doesn't paint) is
    // installed on top of the content window as its sibling, and the arrow layer
    // is added to that clip layer.
    clip_layer_.reset(new ui::Layer(ui::LAYER_NOT_DRAWN));
    clip_layer_->SetBounds(window->layer()->bounds());
    clip_layer_->SetMasksToBounds(true);
    clip_layer_->Add(arrow_.get());

    ui::Layer* parent = window->layer()->parent();
    parent->Add(clip_layer_.get());
    parent->StackAtTop(clip_layer_.get());
}

} // namespace content
